/* Copyright Thanh-Trung Pham, JAIST, 2013 */
package jp.ac.jaist.cli;

import jp.ac.jaist.control.RMPIController;
import jp.ac.jaist.input.RMPIIAWrapper;
import jp.ac.jaist.input.RMPIInputModeler;
import jp.ac.jaist.message.RMPICLIException;
import jp.ac.jaist.message.RMPIMessageResource;
import jp.ac.jaist.simulation.RMPIReliabilitySimulationListener;
import jp.ac.jaist.simulation.RMPIReliabilitySimulationStatistics;
import org.apache.commons.cli.*;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

/**
 * @author Thanh-Trung Pham
 */
public class RMPITool implements RMPIReliabilitySimulationListener {
    public static final int RMPI_NUMBER_OF_RUNS = 1000000;
    //public static final int RMPI_NUMBER_OF_RUNS = 10000;

    private static final String USAGE = "[-h] [-v] [-p] [-s] <input_model_file> <output_file>";
    private static final String HEADER = "A reliability prediction tool of RMPI, Copyright 2014 (c) Thanh-Trung Pham";
    private static final String FOOTER = "For more instructions, refer to our website at http://rmpi.codeplex.com";

    private String m_oCommandLineArgs[];
    private int m_iCurrentProgress = 0;

    private RMPIInputModeler m_oInputModel;
    private RMPIController m_oController;

    StringBuilder m_oOutputContent = new StringBuilder();

    /**
     * The constructor.
     *
     * @param commandLineArgs an array of command line arguments.
     */
    public RMPITool(String[] commandLineArgs) {
        m_oCommandLineArgs = commandLineArgs;
    }

    /**
     * Start the reliability modeling tool.
     */
    private void doStart() {
        OptionGroup oOptionGroup = new OptionGroup();
        oOptionGroup.addOption(OptionBuilder.hasArg(false).withDescription("Print the usage information").create('h'));
        oOptionGroup.addOption(OptionBuilder.hasArg(false).withDescription("Only conduct a validation on the input model").create('v'));
        oOptionGroup.addOption(OptionBuilder.hasArg(false).withDescription("Only conduct a prediction on the input model").create('p'));
        oOptionGroup.addOption(OptionBuilder.hasArg(false).withDescription("Only conduct a simulation on the input model").create('s'));

        CommandLineParser oParser = new BasicParser();
        Options oOptions = new Options();
        oOptions.addOptionGroup(oOptionGroup);

        try {
            CommandLine oCommandLine = oParser.parse(oOptions, m_oCommandLineArgs);
            if (oCommandLine.hasOption('h')) {
                printUsageInformation(oOptions);
                return;
            }

            String[] args = oCommandLine.getArgs();
            if (args == null || args.length != 2 || StringUtils.isBlank(args[0]) || StringUtils.isBlank(args[1])) {
                printInformation("Bad command line arguments were provided!\n", false);
                printUsageInformation(oOptions);
                return;
            }

            String strOutputFilename = args[1];


            String strInputFileName = args[0];

            validateInputReliabilityModeling(strInputFileName);
            if (!m_oInputModel.isValid()) {
                IOUtils.write(m_oOutputContent.toString(), new java.io.FileOutputStream(strOutputFilename));
                return;
            }

            if (oCommandLine.hasOption('v')) {
                IOUtils.write(m_oOutputContent.toString(), new java.io.FileOutputStream(strOutputFilename));
                return;
            }

            if (oCommandLine.hasOption('p')) {
                printInformation("Conducting a reliabilty prediction... ", true);
                conductReliabilityPrediction(strOutputFilename);

                IOUtils.write(m_oOutputContent.toString(), new java.io.FileOutputStream(strOutputFilename));

                printInformation("Done.", false);
                printInformation("Check the output file for detailed results.", true);
                return;
            }

            if (oCommandLine.hasOption('s')) {
                printInformation("Conducting a simulation... ", true);

                conductReliabilitySimulation(RMPI_NUMBER_OF_RUNS);
                IOUtils.write(m_oOutputContent.toString(), new java.io.FileOutputStream(strOutputFilename));

                printInformation("Done.", false);
                printInformation("Check the output file for detailed results.", true);
                return;
            }
        } catch (IOException oIOException) {
            RMPICLIException oRMCLIException = new RMPICLIException(RMPIMessageResource.CLI_UNEXPECTED_EXCEPTION, oIOException, false);
            printErrorInformation(oRMCLIException);
        } catch (ParseException oParseException) {
            printErrorInformation(oParseException);
            printUsageInformation(oOptions);
        } catch (InterruptedException oInterruptedException) {
            RMPICLIException oRMCLIException = new RMPICLIException(RMPIMessageResource.CLI_UNEXPECTED_EXCEPTION, oInterruptedException, false);
            printErrorInformation(oRMCLIException);
        }
    }

    /**
     * Print the error information.
     *
     * @param exception the exception caused the error.
     */
    private void printErrorInformation(Exception exception) {
        System.out.println(exception.getMessage());
    }

    /**
     * The main method.
     *
     * @param args an array of command line arguments.
     */
    public static void main(String args[]) {
        RMPITool oModeling = new RMPITool(args);
        oModeling.doStart();
    }

    /**
     * Print the usage information.
     *
     * @param oOptions the command line options.
     */
    private void printUsageInformation(Options oOptions) {
        HelpFormatter oHelpFormatter = new HelpFormatter();
        oHelpFormatter.setWidth(80);
        oHelpFormatter.printHelp(USAGE, HEADER, oOptions, FOOTER);
    }

    /**
     * Print information.
     *
     * @param information information to print.
     * @param isOnNewLine if true, the information will be on a new line, otherwise false.
     */
    private void printInformation(String information, boolean isOnNewLine) {
        if (isOnNewLine) {
            System.out.println();
        }

        System.out.print(information);
    }

    /**
     * Validate the input model.
     *
     * @param strInputModelFilename the input model file name.
     */
    private void validateInputReliabilityModeling(String strInputModelFilename) {
        printInformation("Validating the input model... ", false);

        m_oController = RMPIController.newInstance();
        m_oInputModel = m_oController.validateInputModelFromFile(strInputModelFilename, m_oOutputContent);

        printInformation("Done.", false);
        if(m_oInputModel.isValid()){
            printInformation("The input model is valid.", true);
        }else{
            printInformation("The input model is not valid. Check the output file for more details!", true);
        }
    }

    /**
     * Conduct a reliability prediction for the input model.
     * @param outputFilename the output filename.
     */
    private void conductReliabilityPrediction(String outputFilename) throws IOException{
        File oOutputFile = new File(outputFilename);
        String strOutputFilename = oOutputFile.getName();
        String strOutputFilenameWithoutExtension = StringUtils.substringBeforeLast(strOutputFilename, ".");

        File oGNUPLOTFolder = new File(oOutputFile.getParentFile(), strOutputFilenameWithoutExtension + "_gnuplots");

        if (!oGNUPLOTFolder.exists()) {
            oGNUPLOTFolder.mkdirs();
        } else {
            FileUtils.cleanDirectory(oGNUPLOTFolder);
        }

        String strGNUPLOTPLTFilename = "System failure probabilities per failure type.plt";
        StringBuilder oGnuplotPLT = new StringBuilder();
        oGnuplotPLT.append("reset").append("\n");
        oGnuplotPLT.append("set nokey").append("\n");
        oGnuplotPLT.append("set title \"System failure probabilities per failure type\"").append("\n");
        oGnuplotPLT.append("set xlabel \"Failure type\"").append("\n");
        oGnuplotPLT.append("set ylabel \"System failure probability\"").append("\n");
        oGnuplotPLT.append("set boxwidth 0.5").append("\n");
        oGnuplotPLT.append("set style fill solid 0.5 border -1").append("\n");
        oGnuplotPLT.append("set xtic rotate by 45 right").append("\n");
        oGnuplotPLT.append("plot \"../").append(strOutputFilename).append("\" using 1:3:xtic(2) with boxes linecolor rgb \"blue\"").append("\n");
        IOUtils.write(oGnuplotPLT.toString(), new java.io.FileOutputStream(new File(oGNUPLOTFolder, strGNUPLOTPLTFilename)));

        RMPIController oController = RMPIController.newInstance();
        RMPIIAWrapper oIAWrapper = oController.conductReliabilityPrediction(m_oInputModel);

        m_oOutputContent.append("#====================================================================\n");
        m_oOutputContent.append("# Predicted system reliability: ").append(oIAWrapper.getProbability(new HashSet<String>(), new HashSet<String>())).append("\n");
        m_oOutputContent.append("# Column 1: Failure type index").append("\n");
        m_oOutputContent.append("# Column 2: Failure type name").append("\n");
        m_oOutputContent.append("# Column 3: System failure probability").append("\n");

        m_oOutputContent.append("# gnuplot plt file: '").append(oGNUPLOTFolder.getName()).append(File.separator).append(strGNUPLOTPLTFilename).append("'\n");

        Set<Set<String>> oAFS = m_oInputModel.getAFS();
        Set<Set<String>> oAIOS = m_oInputModel.getAIOS();

        int iFailureTypeIndex = 0;
        for (Set<String> oF : oAFS) {
            m_oOutputContent.append(iFailureTypeIndex++).append("\t\"").append(getSetOfFailureTypesAsString(oF)).append("\"\t").append(oIAWrapper.getProbability(RMPIInputModeler.RMPI_F0_AS_SET, oF)).append("\n");
        }

        for (Set<String> oO : oAIOS) {
            if (!oO.isEmpty()) {
                m_oOutputContent.append(iFailureTypeIndex++).append("\t\"").append(getSetOfFailureTypesAsString(oO)).append("\"\t").append(oIAWrapper.getProbability(RMPIInputModeler.RMPI_F0_AS_SET, oO)).append("\n");
            }
        }
    }


    /**
     * Get a set of failure types as string.
     *
     * @param setOfFailureTypes a set of failure types.
     * @return the set of failure types as string.
     */
    private String getSetOfFailureTypesAsString(Set<String> setOfFailureTypes) {
        String strList = "";
        if (setOfFailureTypes.isEmpty()) {
            return "{F0}";
        }

        for (String strFailureType : setOfFailureTypes) {
            if (strList.equals("")) {
                strList += "{" + strFailureType;
            } else {
                strList += ("," + strFailureType);
            }
        }

        return strList + "}";
    }

    /**
     * Conduct a reliability simulation for the input model.
     * @param numberOfRuns the number of runs.
     */
    private void conductReliabilitySimulation(int numberOfRuns) throws InterruptedException {
        m_oOutputContent.append("#====================================================================\n");

        RMPIController oController = RMPIController.newInstance();
        oController.conductReliabilitySimulation(m_oInputModel, numberOfRuns, new RMPIReliabilitySimulationListener[]{this});

        Thread oDisplayThread = new Thread(new Runnable() {
            public void run() {
                while (m_iCurrentProgress != -1) {
                    int iPercentage = (m_iCurrentProgress * 100) / RMPI_NUMBER_OF_RUNS;

                    int iLength = String.valueOf(iPercentage).length() + 1;
                    printInformation(iPercentage + "%", false);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException oException) {
                        //do nothing
                    }
                    while (iLength-- > 0) {
                        System.out.print('\b');
                    }
                }
            }
        });

        oDisplayThread.setPriority(Thread.MIN_PRIORITY);
        oDisplayThread.start();
        oDisplayThread.join();
    }

    @Override
    public void informProgress(int numberOfCompletedRuns) {
        m_iCurrentProgress = numberOfCompletedRuns;
    }

    @Override
    public void informCompletion(RMPIReliabilitySimulationStatistics simulationStatistics) {
        m_iCurrentProgress = -1;
        //int iTotalNumberOfRuns = 0;
        int iTotalNumberOfRuns = simulationStatistics.getToTalNumberOfRuns();
        Set<Set<String>> oAFS = m_oInputModel.getAFS();
        Set<Set<String>> oAIOS = m_oInputModel.getAIOS();

        for (Set<String> oF : oAFS) {
            Integer oCounter = simulationStatistics.getCounter(oF);
            //iTotalNumberOfRuns += oCounter.intValue();
            m_oOutputContent.append("# The number of runs ends with a signaled failure of ").append(getSetOfFailureTypesAsString(oF)).append(": ").append(oCounter).append("\n");
        }

        for (Set<String> oO : oAIOS) {
            Integer oCounter = simulationStatistics.getCounter(oO);
            //iTotalNumberOfRuns += oCounter.intValue();

            if (oO.isEmpty()) {
                m_oOutputContent.append("# The number of runs ends with a correct output: ").append(oCounter).append("\n");
            } else {
                m_oOutputContent.append("# The number of runs ends with an erroneous output of ").append(getSetOfFailureTypesAsString(oO)).append(": ").append(oCounter).append("\n");
            }
        }

        m_oOutputContent.append("# The total number of runs: ").append(iTotalNumberOfRuns).append("\n");

        for (Set<String> oF : oAFS) {
            m_oOutputContent.append("# Simulated probability for a signaled failure of ").append(getSetOfFailureTypesAsString(oF)).append(": ").append(((double) simulationStatistics.getCounter(oF)) / iTotalNumberOfRuns).append("\n");

        }

        for (Set<String> oO : oAIOS) {
            if (oO.isEmpty()) {
                m_oOutputContent.append("# Simulated probability for a correct output: ").append(((double) simulationStatistics.getCounter(oO)) / iTotalNumberOfRuns).append("\n");
            } else {
                m_oOutputContent.append("# Simulated probability for an erroneous output of ").append(getSetOfFailureTypesAsString(oO)).append(": ").append(((double) simulationStatistics.getCounter(oO)) / iTotalNumberOfRuns).append("\n");
            }

        }
    }
}
